package org.apache.tomcat.util.digester;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.catalina.startup.Catalina;
import org.apache.tomcat.util.IntrospectionUtils;
import org.apache.tomcat.util.IntrospectionUtils.PropertySource;
import org.xml.sax.ContentHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.ext.EntityResolver2;
import org.xml.sax.ext.LexicalHandler;

/**
 * 读取 server.xml 用于将XML转换为java对象的时间驱动型工具
 *
 * @author chentudong
 * @date 2024/2/19 14:09
 * @since 1.0
 */
public class Digester extends DefaultHandler2 {


  /**
   * Do we want to use the Context ClassLoader when loading classes for instantiating new
   * objects.Default is <code>false</code>. 是否要使用（线程）上下文中的类加载器
   */
  protected boolean useContextClassLoader = false;

  /**
   * The "root" element of the stack (in other words, the last object that was popped.
   * 这个root对象就是Catalina对象
   */
  protected Object root = null;

  /**
   * The <code>Rules</code> implementation containing our collection of
   * <code>Rule</code> instances and associated matching policy.  If not
   * established before the first rule is added, a default implementation will be provided.
   */
  protected Rules rules = null;

  /**
   * The object stack being constructed.
   */
  protected ArrayStack<Object> stack = new ArrayStack<>();

  /**
   * Has this Digester been configured yet.
   */
  protected boolean configured = false;

  /**
   * The XMLReader used to parse digester rules.
   */
  protected XMLReader reader = null;

  /**
   * The SAXParser we will use to parse the input stream.
   */
  protected SAXParser parser = null;

  /**
   * The SAXParserFactory that is created the first time we need it.
   */
  protected SAXParserFactory factory = null;

  /**
   * The EntityResolver used by the SAX parser. By default it use this class
   */
  protected EntityResolver entityResolver;
  protected IntrospectionUtils.PropertySource[] source;

  /**
   * The class loader to use for instantiating application objects. If not specified, the context
   * class loader, or the class loader used to load Digester itself, is used, based on the value of
   * the
   * <code>useContextClassLoader</code> variable.
   */
  protected ClassLoader classLoader = null;

  public Digester() {
    List<PropertySource> sourcesList = new ArrayList<>();
    sourcesList.add(new SystemPropertySource());
    source = sourcesList.toArray(new IntrospectionUtils.PropertySource[0]);
  }

  /**
   * Set the class loader to be used for instantiating application objects when required.
   *
   * @param classLoader The new class loader to use, or <code>null</code> to revert to the standard
   *                    rules
   */
  public void setClassLoader(ClassLoader classLoader) {
    this.classLoader = classLoader;
  }

  public boolean isUseContextClassLoader() {
    return useContextClassLoader;
  }

  public void setUseContextClassLoader(boolean useContextClassLoader) {
    this.useContextClassLoader = useContextClassLoader;
  }

  /**
   * 添加对象规则
   *
   * @param pattern       pattern
   * @param className     className
   * @param attributeName attributeName
   */
  public void addObjectCreate(String pattern, String className, String attributeName) {
    addRule(pattern, new ObjectCreateRule(className, attributeName));
  }

  /**
   * Add a "set properties" rule for the specified parameters.
   *
   * @param pattern pattern
   */
  public void addSetProperties(String pattern) {
    addRule(pattern, new SetPropertiesRule());
  }

  /**
   * 添加规则
   */
  public void addRule(String pattern, Rule rule) {
    rule.setDigester(this);
    getRules().add(pattern, rule);
  }

  /**
   * getRules
   *
   * @return Rules
   */
  public Rules getRules() {
    if (Objects.isNull(this.rules)) {
      this.rules = new RulesBase();
      this.rules.setDigester(this);
    }
    return this.rules;
  }

  /**
   * 添加对象到栈
   *
   * @param object object
   */
  public void push(Object object) {
    if (stack.isEmpty()) {
      root = object;
    }
    stack.push(object);
  }

  public Object parse(InputSource input) throws SAXException, IOException {
    configure();
    //解析xml，并通过反射构建StandardServer实例，同时也包括engine、host实例
    //以及还会往Server组件中添加listener 逐个节点的解析
    getXMLReader().parse(input);
    return root;
  }

  public XMLReader getXMLReader() throws SAXException {
    if (Objects.isNull(reader)) {
      reader = getParser().getXMLReader();
    }
    reader.setDTDHandler(this);
    reader.setContentHandler(this);
    EntityResolver entityResolver = getEntityResolver();
    if (Objects.isNull(entityResolver)) {
      entityResolver = this;
    }
    entityResolver = new EntityResolver2Wrapper((EntityResolver2) entityResolver, source,
        classLoader);
    reader.setEntityResolver(entityResolver);
    reader.setProperty("http://xml.org/sax/properties/lexical-handler", this);
    reader.setErrorHandler(this);
    return reader;
  }

  /**
   * @return the SAXParser we will use to parse the input stream.  If there is a problem creating
   * the parser, return <code>null</code>. 我们将使用SAXParser来解析输入流。如果创建解析器有问题，则返回<code>null<code>。
   */
  public SAXParser getParser() {
    if (Objects.nonNull(parser)) {
      return parser;
    }
    try {
      parser = getFactory().newSAXParser();
    } catch (ParserConfigurationException | SAXException e) {
      e.printStackTrace();
      return null;
    }
    return parser;
  }

  public SAXParserFactory getFactory() {
    if (Objects.isNull(factory)) {
      factory = SAXParserFactory.newInstance();
      factory.setNamespaceAware(false);
      factory.setValidating(false);
    }
    return factory;
  }

  protected void configure() {
    // Do not configure more than once
    if (configured) {
      return;
    }
    // Set the configuration flag to avoid repeating
    configured = true;
  }

  /**
   * Return the Entity Resolver used by the SAX parser.
   *
   * @return Return the Entity Resolver used by the SAX parser.
   */
  public EntityResolver getEntityResolver() {
    return entityResolver;
  }

  private static class EntityResolverWrapper implements EntityResolver {

    private final EntityResolver entityResolver;
    private final PropertySource[] source;
    private final ClassLoader classLoader;

    public EntityResolverWrapper(EntityResolver entityResolver, PropertySource[] source,
        ClassLoader classLoader) {
      this.entityResolver = entityResolver;
      this.source = source;
      this.classLoader = classLoader;
    }

    /**
     * Allow the application to resolve external entities.
     *
     * <p>The parser will call this method before opening any external
     * entity except the top-level document entity.  Such entities include the external DTD subset
     * and external parameter entities referenced within the DTD (in either case, only if the parser
     * reads external parameter entities), and external general entities referenced within the
     * document element (if the parser reads external general entities).  The application may
     * request that the parser locate the entity itself, that it use an alternative URI, or that it
     * use data provided by the application (as a character or byte input stream).</p>
     *
     * <p>Application writers can use this method to redirect external
     * system identifiers to secure and/or local URIs, to look up public identifiers in a catalogue,
     * or to read an entity from a database or other input source (including, for example, a dialog
     * box). Neither XML nor SAX specifies a preferred policy for using public or system IDs to
     * resolve resources.  However, SAX specifies how to interpret any InputSource returned by this
     * method, and that if none is returned, then the system ID will be dereferenced as a URL.
     * </p>
     *
     * <p>If the system identifier is a URL, the SAX parser must
     * resolve it fully before reporting it to the application.</p>
     *
     * @param publicId The public identifier of the external entity being referenced, or null if
     *                 none was supplied.
     * @param systemId The system identifier of the external entity being referenced.
     * @return An InputSource object describing the new input source, or null to request that the
     * parser open a regular URI connection to the system identifier.
     * @throws SAXException Any SAX exception, possibly wrapping another exception.
     * @throws IOException  A Java-specific IO exception, possibly the result of creating a new
     *                      InputStream or Reader for the InputSource.
     * @see InputSource
     */
    @Override
    public InputSource resolveEntity(String publicId, String systemId)
        throws SAXException, IOException {
      return null;
    }
  }

  private static class EntityResolver2Wrapper extends EntityResolverWrapper implements
      EntityResolver2 {

    private final EntityResolver2 entityResolver2;

    public EntityResolver2Wrapper(EntityResolver2 entityResolver, PropertySource[] source,
        ClassLoader classLoader) {
      super(entityResolver, source, classLoader);
      this.entityResolver2 = entityResolver;
    }

    /**
     * Allows applications to provide an external subset for documents that don't explicitly define
     * one. Documents with DOCTYPE declarations that omit an external subset can thus augment the
     * declarations available for validation, entity processing, and attribute processing
     * (normalization, defaulting, and reporting types including ID). This augmentation is reported
     * through the {@link LexicalHandler#startDTD startDTD()} method as if the document text had
     * originally included the external subset; this callback is made before any internal subset
     * data or errors are reported.</p>
     *
     * <p>This method can also be used with documents that have no DOCTYPE
     * declaration.  When the root element is encountered, but no DOCTYPE declaration has been seen,
     * this method is invoked.  If it returns a value for the external subset, that root element is
     * declared to be the root element, giving the effect of splicing a DOCTYPE declaration at the
     * end the prolog of a document that could not otherwise be valid.  The sequence of parser
     * callbacks in that case logically resembles this:</p>
     *
     * <pre>
     * ... comments and PIs from the prolog (as usual)
     * startDTD ("rootName", source.getPublicId (), source.getSystemId ());
     * startEntity ("[dtd]");
     * ... declarations, comments, and PIs from the external subset
     * endEntity ("[dtd]");
     * endDTD ();
     * ... then the rest of the document (as usual)
     * startElement (..., "rootName", ...);
     * </pre>
     *
     * <p>Note that the InputSource gets no further resolution.
     * Implementations of this method may wish to invoke {@link #resolveEntity resolveEntity()} to
     * gain benefits such as use of local caches of DTD entities.  Also, this method will never be
     * used by a (non-validating) processor that is not including external parameter entities. </p>
     *
     * <p>Uses for this method include facilitating data validation when
     * interoperating with XML processors that would always require undesirable network accesses for
     * external entities, or which for other reasons adopt a "no DTDs" policy. Non-validation
     * motives include forcing documents to include DTDs so that attributes are handled
     * consistently. For example, an XPath processor needs to know which attibutes have type "ID"
     * before it can process a widely used type of reference.</p>
     *
     * <p><strong>Warning:</strong> Returning an external subset modifies
     * the input document.  By providing definitions for general entities, it can make a malformed
     * document appear to be well formed.
     * </p>
     *
     * @param name    Identifies the document root element.  This name comes from a DOCTYPE
     *                declaration (where available) or from the actual root element.
     * @param baseURI The document's base URI, serving as an additional hint for selecting the
     *                external subset.  This is always an absolute URI, unless it is null because
     *                the XMLReader was given an InputSource without one.
     * @return An InputSource object describing the new external subset to be used by the parser, or
     * null to indicate that no external subset is provided.
     * @throws SAXException Any SAX exception, possibly wrapping another exception.
     * @throws IOException  Probably indicating a failure to create a new InputStream or Reader, or
     *                      an illegal URL.
     */
    @Override
    public InputSource getExternalSubset(String name, String baseURI)
        throws SAXException, IOException {
      return null;
    }

    /**
     * Allows applications to map references to external entities into input sources, or tell the
     * parser it should use conventional URI resolution. This method is only called for external
     * entities which have been properly declared. This method provides more flexibility than the
     * {@link EntityResolver} interface, supporting implementations of more complex catalogue
     * schemes such as the one defined by the <a href=
     * "http://www.oasis-open.org/committees/entity/spec-2001-08-06.html" >OASIS XML Catalogs</a>
     * specification.</p>
     *
     * <p>Parsers configured to use this resolver method will call it
     * to determine the input source to use for any external entity being included because of a
     * reference in the XML text. That excludes the document entity, and any external entity
     * returned by {@link #getExternalSubset getExternalSubset()}. When a (non-validating) processor
     * is configured not to include a class of entities (parameter or general) through use of
     * feature flags, this method is not invoked for such entities.  </p>
     *
     * <p>Note that the entity naming scheme used here is the same one
     * used in the {@link LexicalHandler}, or in the
     * {@link ContentHandler#skippedEntity ContentHandler.skippedEntity()} method. </p>
     *
     * @param name     Identifies the external entity being resolved. Either "[dtd]" for the
     *                 external subset, or a name starting with "%" to indicate a parameter entity,
     *                 or else the name of a general entity.  This is never null when invoked by a
     *                 SAX2 parser.
     * @param publicId The public identifier of the external entity being referenced (normalized as
     *                 required by the XML specification), or null if none was supplied.
     * @param baseURI  The URI with respect to which relative systemIDs are interpreted.  This is
     *                 always an absolute URI, unless it is null (likely because the XMLReader was
     *                 given an InputSource without one).  This URI is defined by the XML
     *                 specification to be the one associated with the "&lt;" starting the relevant
     *                 declaration.
     * @param systemId The system identifier of the external entity being referenced; either a
     *                 relative or absolute URI. This is never null when invoked by a SAX2 parser;
     *                 only declared entities, and any external subset, are resolved by such
     *                 parsers.
     * @return An InputSource object describing the new input source to be used by the parser.
     * Returning null directs the parser to resolve the system ID against the base URI and open a
     * connection to resulting URI.
     * @throws SAXException Any SAX exception, possibly wrapping another exception.
     * @throws IOException  Probably indicating a failure to create a new InputStream or Reader, or
     *                      an illegal URL.
     */
    @Override
    public InputSource resolveEntity(String name, String publicId, String baseURI, String systemId)
        throws SAXException, IOException {
      return null;
    }
  }
}
